/*
 * Board game - put the figures down and try to make a line.
 * Copyright (C)  2008 Georgi Staykov
 * 
 * This file is part of ClearTheLines.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.gstaykov.clearthelines;

import java.awt.Canvas;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import com.gstaykov.clearthelines.figures.AbstractFigure;
import com.gstaykov.clearthelines.figures.Figure1;
import com.gstaykov.clearthelines.figures.Figure2;
import com.gstaykov.clearthelines.figures.Figure3;
import com.gstaykov.clearthelines.figures.Figure4;
import com.gstaykov.clearthelines.figures.Figure5;
import com.gstaykov.clearthelines.figures.Figure6;
import com.gstaykov.clearthelines.figures.Figure7;

public class ClearTheLines {
    private static final String[] FIGURE_FILENAMES = new String[] {"Background.gif", "RedBlock.gif", "GreenBlock.gif", "YellowBlock.gif", "OrangeBlock.gif", "BlueBlock.gif", "VioletBlock.gif",
                                                                   "DWhiteBlock.gif"};
    private static final int BLOCK_SIZE_PIXELS = 20;
    private static final int BLOCKS_PER_LINE = 10;
    private static final int BLOCKS_PER_ROW = 25;
    private static final int NF_BLOCKS_PER_LINE = 4;
    private static final int NF_BLOCKS_PER_ROW = 4;
    private static final int FPS = 50;
    private static final int LINES_PER_LEVEL = 50;
    private static final int DROP_DELAY_MS = 1000;
    private static final int MAXIMUM_LEVEL = 10;
    private static String resoursesPath = "Resourses\\";

    private JFrame mainWindow = null;
    private GameActionPanel gamePanel = null;
    private JPanel infoPanel = null;
    private ImageMatrix playGround = new ImageMatrix(BLOCKS_PER_LINE, BLOCKS_PER_ROW);
    private ImageMatrix nextFigureMatrix = new ImageMatrix(NF_BLOCKS_PER_LINE, NF_BLOCKS_PER_ROW);
    private BufferedImage[] blocks = new BufferedImage[FIGURE_FILENAMES.length + 1];
    private AbstractFigure[] figures = new AbstractFigure[] {new Figure1(), new Figure2(), new Figure3(), new Figure4(), new Figure5(), new Figure6(), new Figure7()};
    private AbstractFigure[] nextFigures = new AbstractFigure[] {new Figure1(), new Figure2(), new Figure3(), new Figure4(), new Figure5(), new Figure6(), new Figure7()};
    private Random random = new Random(System.currentTimeMillis());
    private int currentScore = 0;
    private int totalLinesRemoved = 0;
    private JTextField scoreBoxText = null;
    private JTextField clearedLinesText = null;
    private JTextField currentLevelText = null;
    private AbstractFigure currentFigure = null;
    private AbstractFigure nextFigure = null;
    private Canvas nextFigurePanel = null;
    private int currentLevel = 1;
    private boolean gameOver = false;
    private Timer timer = null;
    private RecordsManager recordsManager = null;

    public static void main(String[] args) {
        ClearTheLines ctd = new ClearTheLines();
        ctd.initResources();
        ctd.initMainWindow();
        ctd.runMainLoop();
    }

    public String getFullResourcePath(String filename) {
        return resoursesPath + filename;
    }

    public JFrame getMainWindow() {
        return mainWindow;
    }

    private Graphics2D drawImageMatrix(Graphics2D graphics, BufferStrategy bufferStrategy, ImageMatrix imageMatrix) {
        for (int i = 0; i < imageMatrix.getSizeX(); i++) {
            for (int j = 0; j < imageMatrix.getSizeY(); j++) {
                graphics.drawImage(blocks[imageMatrix.getMatrixValue(i, j)], null, i * BLOCK_SIZE_PIXELS, j * BLOCK_SIZE_PIXELS);
            }
        }
        graphics.dispose();
        bufferStrategy.show();
        return (Graphics2D)bufferStrategy.getDrawGraphics();
    }

    /**
     * Processes the user keyboard input
     * @param figure
     */
    private void processUserInput(AbstractFigure figure, ImageMatrix playground) {
        int currentKey = gamePanel.getCurrentKey();
        switch (currentKey) {
            case KeyEvent.VK_RIGHT: figure.goRight(playground); break;
            case KeyEvent.VK_LEFT: figure.goLeft(playground); break;
            case KeyEvent.VK_DOWN: figure.goDown(playground); break;
            case KeyEvent.VK_UP: figure.rotate(playground); break;
            case KeyEvent.VK_SPACE: while (dropFigure()); break;
        }
    }

    private void sleepBetweenFrames(int FPS) {
        try {
            Thread.sleep(1000 / FPS);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }

    private int removeFullLines(ImageMatrix playground) {
        int linesRemoved = 0;
        
        for (int i = BLOCKS_PER_ROW - 1; i > 0; i--) {
            boolean isClean = true;
            for (int j = 0; j < BLOCKS_PER_LINE; j++) {
                if (playground.getMatrixValue(j, i) == 0) {isClean = false;}
            }
            if (isClean) {
                for (int k = i; k > 0; k--) {
                    for (int j = 0; j < BLOCKS_PER_LINE; j++) {
                        playground.setMatrixValue(j, k, playground.getMatrixValue(j, k - 1));
                    }
                }
                for (int j = 0; j < BLOCKS_PER_LINE; j++) {
                    playground.setMatrixValue(j, 0, 0);
                }
                linesRemoved++;
                i++;
            }
        }
        return linesRemoved;
    }

    private void loadImages() {
        for (int i = 0; i < FIGURE_FILENAMES.length; i++) {
            try {
                blocks[i] = ImageIO.read(new File(getFullResourcePath(FIGURE_FILENAMES[i])));
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }

    private AbstractFigure getNextFigure() {
        BufferStrategy nextFigureBufferStrategy = nextFigurePanel.getBufferStrategy();
        Graphics2D nextFigureGraphics = (Graphics2D)nextFigureBufferStrategy.getDrawGraphics();
        if (nextFigure == null) {
            nextFigure = nextFigures[random.nextInt(figures.length)];
        }
        currentFigure = nextFigure;
        nextFigure = nextFigures[random.nextInt(figures.length)];
        nextFigureMatrix.fillMatrix(0);
        nextFigure.init(nextFigureMatrix);
        drawImageMatrix(nextFigureGraphics, nextFigureBufferStrategy, nextFigureMatrix);
        
        return currentFigure;
    }

    private boolean dropFigure() {
        boolean opSuccess = currentFigure.goDown(playGround);
        if (!opSuccess) {
            int linesRemoved = removeFullLines(playGround);
            if (linesRemoved != 0) {
                currentScore += ((100 + 15 * (currentLevel - 1)) * linesRemoved + (50 + 4 * (currentLevel - 1)) * (linesRemoved - 1));
                scoreBoxText.setText("" + currentScore);
                totalLinesRemoved += linesRemoved;
                currentLevel = 1 + (int)(totalLinesRemoved / LINES_PER_LEVEL);
                if (currentLevel > MAXIMUM_LEVEL) {
                    gameOver = true;
                }
                currentLevelText.setText("" + currentLevel);
                clearedLinesText.setText("" + totalLinesRemoved);
                mainWindow.validate();
                mainWindow.repaint();
            }
            currentFigure = getNextFigure();
            if (!currentFigure.init(playGround)) {
                gameOver = true;
            }
        }
        return opSuccess;
    }

    private void initResources() {
        playGround.fillMatrix(0);
        nextFigureMatrix.fillMatrix(0);
        recordsManager = new RecordsManager(this);
        recordsManager.loadRecords();
        loadImages();
        currentLevel = 1;
        currentScore = 0;
        totalLinesRemoved = 0;
        gameOver = false;
    }

    private void runMainLoop() {
        BufferStrategy bufferStrategy = gamePanel.getBufferStrategy();
        Graphics2D graphics = (Graphics2D)bufferStrategy.getDrawGraphics();
        
        while(true) {
            currentLevelText.setText("" + currentLevel);
            scoreBoxText.setText("" + currentScore);
            clearedLinesText.setText("" + totalLinesRemoved);
            gamePanel.setIgnoreRepaint(true);
            nextFigurePanel.setIgnoreRepaint(true);

            currentFigure = getNextFigure();
            currentFigure.init(playGround);
            timer = new Timer(this);
            timer.start();
            while (!gameOver) {
                processUserInput(currentFigure, playGround);
                graphics = drawImageMatrix(graphics, bufferStrategy, playGround);
                sleepBetweenFrames(FPS);
            }
            timer.interrupt();
            gamePanel.setIgnoreRepaint(false);
            recordsManager.saveRecords(currentScore, currentLevel);
            initResources();
            mainWindow.remove(infoPanel);
            infoPanel = initInfoBoard();
            mainWindow.add(infoPanel);
            mainWindow.validate();
            mainWindow.repaint();
        }
    }

    private int getSleepTimeout() {
        return DROP_DELAY_MS - 85 * currentLevel;
    }

    private GameActionPanel initGamePanel() {
        gamePanel = new GameActionPanel();
        gamePanel.setSize(BLOCKS_PER_LINE * BLOCK_SIZE_PIXELS, BLOCKS_PER_ROW * BLOCK_SIZE_PIXELS);
        gamePanel.setFocusable(true);
        return gamePanel;
    }

    private JPanel initNewGamePanel() {
        JPanel result = new JPanel(new FlowLayout());
        JButton newGameButton = new JButton("New Game");
        newGameButton.addActionListener(new NewGameButtonActionListener(this));
        result.add(newGameButton);
        return result;
    }

    private JPanel initInfoBoard() {
        JPanel infoBoard = new JPanel(new FlowLayout());
        
        JPanel statsGrid = new JPanel(new GridLayout(0, 2));
        statsGrid.add(new JLabel("Score: "));
        scoreBoxText = new JTextField();
        scoreBoxText.setEditable(false);
        scoreBoxText.setColumns(8);
        statsGrid.add(scoreBoxText);
        statsGrid.add(new JLabel("Cleared lines: "));
        clearedLinesText = new JTextField();
        clearedLinesText.setEditable(false);
        clearedLinesText.setColumns(8);
        statsGrid.add(clearedLinesText);
        statsGrid.add(new JLabel("Current level: "));
        currentLevelText = new JTextField();
        currentLevelText.setEditable(false);
        currentLevelText.setColumns(2);
        statsGrid.add(currentLevelText);
        
        JPanel nextFigureGrid = new JPanel(new GridLayout(0, 2));
        nextFigurePanel = new Canvas();
        nextFigurePanel.setSize(NF_BLOCKS_PER_LINE * BLOCK_SIZE_PIXELS, NF_BLOCKS_PER_ROW * BLOCK_SIZE_PIXELS);
        nextFigureGrid.add(new JLabel("Next figure:"));
        nextFigureGrid.add(nextFigurePanel);
        
        infoBoard.add(statsGrid);
        infoBoard.add(nextFigureGrid);
        infoBoard.add(recordsManager.initHallOfFame());
        infoBoard.add(initNewGamePanel());
        return infoBoard;
    }

    private void initMainWindow() {
        GraphicsEnvironment graphEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice graphDevice = graphEnv.getDefaultScreenDevice();
        GraphicsConfiguration graphicConf = graphDevice.getDefaultConfiguration();
        
        mainWindow = new JFrame("Clear the lines", graphicConf);
        mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainWindow.setSize(430, mainWindow.getHeight() + 532);
        mainWindow.setFocusTraversalKeysEnabled(false);
        mainWindow.setLayout(new GridLayout(1, 2));
        mainWindow.add(initGamePanel());
        infoPanel = initInfoBoard();
        mainWindow.add(infoPanel);
        mainWindow.setResizable(false);

        mainWindow.setVisible(true);
        gamePanel.createBufferStrategy(2);
        nextFigurePanel.createBufferStrategy(2);
    }

    private static class Timer extends Thread {
        private ClearTheLines ruler = null;

        public Timer(ClearTheLines ruler) {
            this.ruler = ruler;
        }

        public void run() {
            while (!interrupted()) {
                ruler.dropFigure();
                try {
                    Timer.sleep(ruler.getSleepTimeout());
                } catch (InterruptedException ie) {
                    break; // Most probably caused by game over
                }
            }
        }
    }

    private class NewGameButtonActionListener implements ActionListener {
        private ClearTheLines ruler = null;

        public NewGameButtonActionListener(ClearTheLines ruler) {
            this.ruler = ruler;
        }
        
        public void actionPerformed(ActionEvent e) {
            ruler.gameOver = true;
        }
    }
}
